Normalize imported and rendered color spaces#2411
Normalize imported and rendered color spaces#2411eric-heiden wants to merge 8 commits intonewton-physics:mainfrom
Conversation
Store shape colors in linear RGB so viewers and cameras can apply\noutput encoding explicitly instead of mixing color spaces.\n\nConvert USD-authored material and display colors to linear using USD\ncolor-space metadata, preserve texture color-space metadata, and add a\nSensorTiledCamera option to keep packed outputs linear instead of\nencoding them to sRGB.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughColors and textures are made color‑space aware across import, mesh, rendering, and viewer layers: authored/display sRGB colors are stored, texture color-space metadata is preserved, shading operates in linear light, and packed sensor outputs can be emitted as display sRGB or raw linear bytes controlled by RenderConfig.encode_output_srgb. Changes
Sequence DiagramsequenceDiagram
participant App as Application
participant USD as USD Importer
participant Builder as ModelBuilder
participant Model as Model
participant TexMgr as TextureManager
participant Renderer as Renderer (WARP)
participant Encoder as Output Encoder
participant Viewer as Viewer
App->>USD: import prim (material/texture)
USD->>USD: read sourceColorSpace / SetColorSpace
USD->>Builder: provide color, texture_path, texture_color_space
Builder->>Model: store shape_color (display sRGB) and mesh.texture_color_space
App->>Renderer: render(scene, config.encode_output_srgb)
Renderer->>TexMgr: load/sample texture (uses mesh.texture_color_space)
TexMgr->>TexMgr: if color_space != RAW -> srgb_to_linear(sample)
Renderer->>Renderer: shade in linear RGB using shape_color (converted to linear when needed)
alt config.encode_output_srgb == True
Renderer->>Encoder: linear_to_srgb(shaded) -> pack_rgba_to_uint32
else
Renderer->>Encoder: pack linear RGB bytes -> pack_rgba_to_uint32
end
Encoder->>Viewer: deliver packed pixels (viewer unpacks per encoding)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
newton/_src/usd/utils.py (1)
1935-1947:⚠️ Potential issue | 🟠 MajorExclude
texture_color_spacefrom the "did we resolve anything?" sentinels.After adding
"texture_color_space": "auto"to the properties dict, this branch can now treat an otherwise-empty material as populated, and the merge loop here can never replace"auto"with a resolved direct-input color space. That breaksdisplayColorfallback and can downgrade raw textures back to the default sRGB path.💡 Suggested fix
if source_shader is None: material_props = _extract_material_input_properties(material, target_prim) - if any(value is not None for value in material_props.values()): + if any(material_props.get(key) is not None for key in ("texture", "color", "metallic", "roughness")): return material_props return None @@ - for key in ("texture", "texture_color_space", "color", "metallic", "roughness"): + for key in ("texture", "color", "metallic", "roughness"): if properties.get(key) is None and material_props.get(key) is not None: properties[key] = material_props[key] + if ( + properties.get("texture") is not None + and material_props.get("texture") is not None + and properties.get("texture_color_space") == TEXTURE_COLOR_SPACE_AUTO + ): + properties["texture_color_space"] = material_props["texture_color_space"]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/usd/utils.py` around lines 1935 - 1947, The code treats "texture_color_space":"auto" as a resolved value which prevents later fallbacks and merges; update the logic around _extract_material_input_properties and the merge loop so "texture_color_space" is excluded from the "did we resolve anything?" sentinel (i.e., when checking any(value is not None for value in material_props.values()) ignore the texture_color_space entry) and modify the merge condition for the keys tuple so that for "texture_color_space" you allow overwriting when properties.get("texture_color_space") == "auto" (treat "auto" as unresolved) — change the check from properties.get(key) is None to properties.get(key) is None or (key == "texture_color_space" and properties.get(key) == "auto") while still sourcing values from material_props.newton/_src/sensors/warp_raytrace/render_context.py (1)
701-725:⚠️ Potential issue | 🟡 MinorDon't leave procedural
TextureDataon the implicit color-space default.Lines 701-725 make file-backed textures explicit, but
newton/_src/sensors/warp_raytrace/utils.py, Lines 558-574 still creates the checkerboardTextureData()without settingcolor_space. With the new shading path, that helper no longer states whether its hard-coded0xFF808080/0xFFBFBFBFbytes are raw-linear or sRGB, so it can drift from the rest of the pipeline. Please set that field there as well, instead of relying on the implicit default.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/sensors/warp_raytrace/render_context.py` around lines 701 - 725, The checkerboard TextureData created in the helper (the procedural/file-backed branch uses TextureData with color_space set) must also explicitly set its color_space; update the checkerboard TextureData construction in the helper in warp_raytrace/utils.py to assign data.color_space using texture_color_space_to_id(...) (e.g., the same default used elsewhere such as texture_color_space_to_id("auto") or the appropriate pipeline default) so the checkerboard's color space matches the rest of the shading path; reference the TextureData instance and the texture_color_space_to_id function when making the change.
🧹 Nitpick comments (4)
newton/_src/utils/import_usd.py (1)
527-528: Defershape_colorresolution until a supported geometry type is known.Line 528 now resolves material bindings for every uncached child, including
Xformand other nodes that never create a shape. Moving that lookup behind the supported-geometry branches avoids extra traversal work and cache entries on large USD hierarchies.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/utils/import_usd.py` around lines 527 - 528, The code currently calls _get_material_props_cached(prim) for every uncached child (path_name) even for non-geometry prims; move the shape_color lookup so it only runs after you detect a supported geometry type for prim (e.g., inside the branches that handle Mesh/Sphere/Cube or after checking prim.GetTypeName()/UsdGeom predicates), leaving the existing path_name not in path_shape_map check in place but deferring shape_color resolution until inside the geometry-specific branches where a shape is actually created; use the same variables (path_name, prim, shape_color, _get_material_props_cached, path_shape_map) to locate and update the logic.newton/tests/test_sensor_tiled_camera.py (1)
12-12: Keep the test oracle independent of Newton’s own converter.Line 12 imports
linear_to_srgb_rgb(), and Lines 208-212 use it to derive the expected bytes. That makes the assertion less independent of the production color-space code. A tiny local reference conversion or fixed expected bytes would make this regression test stronger.Also applies to: 208-212
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/tests/test_sensor_tiled_camera.py` at line 12, The test imports and uses Newton’s linear_to_srgb_rgb() converter to build the expected byte results, making the oracle dependent on production code; remove the import of linear_to_srgb_rgb from test_sensor_tiled_camera.py and replace the derived-expected logic (the code around where expected bytes are computed) with either a small, self-contained local conversion implementation (e.g., the sRGB gamma mapping applied to linear RGB values used in the test) or with fixed hard-coded expected byte values; update the assertion to use that local converter or fixed bytes and ensure you do not call linear_to_srgb_rgb() from the production module in the test.newton/_src/sensors/sensor_tiled_camera.py (1)
210-213: Clarify whereencode_output_srgbis configured in theupdate()docs.
update()does not take arender_configargument, sorender_config.encode_output_srgb=Falseis ambiguous here. Consider referencingself.render_config.encode_output_srgb(or setting it at construction viaconfig=SensorTiledCamera.RenderConfig(...)).✏️ Suggested doc wording tweak
- color_image: Output for packed RGBA color. The bytes are sRGB by - default, or linear when - ``render_config.encode_output_srgb=False``. None to skip. + color_image: Output for packed RGBA color. The bytes are sRGB by + default, or linear when + ``self.render_config.encode_output_srgb=False``. None to skip.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/sensors/sensor_tiled_camera.py` around lines 210 - 213, Docstring refers to render_config.encode_output_srgb ambiguously; update the docs to point to the instance config by referencing self.render_config.encode_output_srgb (or note that it can be set at construction via SensorTiledCamera.RenderConfig passed as config) and clarify that update() itself does not accept a render_config argument—update the parameter descriptions for color_image/depth_image accordingly.newton/tests/test_import_usd.py (1)
20-20: Keep these sRGB expectations independent from production code.Importing
srgb_to_linear_rgbfromnewton._src.utils.colormeans these assertions can still pass if the shared conversion logic regresses in the same way on both sides. Prefer fixed reference values, or at least a dedicated test-local reference implementation, for these expectations.Also applies to: 6537-6556, 6558-6585, 6587-6617, 6620-6651
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/tests/test_import_usd.py` at line 20, The test imports the production function srgb_to_linear_rgb which makes the test pass even if both test and production code regress; remove the import of newton._src.utils.color.srgb_to_linear_rgb and replace usage with either hard-coded reference output values (fixed decimals) or a small test-local implementation of the sRGB→linear conversion inside newton/tests/test_import_usd.py, and update the assertions to compare against those fixed values; apply the same change to the other related test blocks referenced (around the ranges 6537-6556, 6558-6585, 6587-6617, 6620-6651) so all sRGB expectations are independent of production code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@newton/_src/sim/builder.py`:
- Around line 5216-5219: The param docstring for the color argument currently
lists fallbacks in the wrong order; update the wording wherever that paragraph
appears (the color param doc in builder functions) to reflect the actual
precedence: first use the explicit color argument, then fall back to src.color
(mesh/source color), then default_shape_color if set, and finally the per-shape
palette sequence (and note Mesh.color is consulted for mesh-backed shapes before
default_shape_color). Locate the docstring section mentioning "color: Optional
linear RGB..." and the similar paragraphs elsewhere and reorder/clarify the
fallback description to match the implemented precedence (color -> src.color ->
default_shape_color -> palette).
- Around line 197-200: The _normalize_shape_color helper must validate inputs
instead of blindly indexing: in _normalize_shape_color (and callers like any use
of default_shape_color), check that color is a sequence/iterable of exactly
three numeric components, raise a clear ValueError for malformed inputs (e.g.,
wrong length or non-numeric items), and then return the tuple of floats via
float(component) conversions; this ensures malformed values like (1.0, 0.0)
produce a descriptive ValueError rather than an IndexError.
In `@newton/_src/usd/utils.py`:
- Around line 1687-1689: The resolved texture color space stored in
properties["texture_color_space"] is not being propagated to the returned Mesh;
modify get_mesh() (and the equivalent branch around 1843-1847) so after
Mesh.create_from_usd(...) (or when populating mesh_out) you set
mesh_out._texture_color_space = properties.get("texture_color_space") (or the
resolved variable) before returning the mesh; ensure this assignment occurs in
both branches that call _find_texture_in_shader/source_shader so callers of
newton.usd.get_mesh() and Mesh.create_from_usd() receive the correct color space
instead of falling back to "auto".
In `@newton/_src/utils/color.py`:
- Line 1: The SPDX header in newton/_src/utils/color.py incorrectly uses 2025 as
the creation year; update the SPDX-FileCopyrightText line at the top of color.py
to use the file’s actual creation year (replace "2025" with the correct creation
year) so the file header follows the guideline of using the year the file was
first created.
---
Outside diff comments:
In `@newton/_src/sensors/warp_raytrace/render_context.py`:
- Around line 701-725: The checkerboard TextureData created in the helper (the
procedural/file-backed branch uses TextureData with color_space set) must also
explicitly set its color_space; update the checkerboard TextureData construction
in the helper in warp_raytrace/utils.py to assign data.color_space using
texture_color_space_to_id(...) (e.g., the same default used elsewhere such as
texture_color_space_to_id("auto") or the appropriate pipeline default) so the
checkerboard's color space matches the rest of the shading path; reference the
TextureData instance and the texture_color_space_to_id function when making the
change.
In `@newton/_src/usd/utils.py`:
- Around line 1935-1947: The code treats "texture_color_space":"auto" as a
resolved value which prevents later fallbacks and merges; update the logic
around _extract_material_input_properties and the merge loop so
"texture_color_space" is excluded from the "did we resolve anything?" sentinel
(i.e., when checking any(value is not None for value in material_props.values())
ignore the texture_color_space entry) and modify the merge condition for the
keys tuple so that for "texture_color_space" you allow overwriting when
properties.get("texture_color_space") == "auto" (treat "auto" as unresolved) —
change the check from properties.get(key) is None to properties.get(key) is None
or (key == "texture_color_space" and properties.get(key) == "auto") while still
sourcing values from material_props.
---
Nitpick comments:
In `@newton/_src/sensors/sensor_tiled_camera.py`:
- Around line 210-213: Docstring refers to render_config.encode_output_srgb
ambiguously; update the docs to point to the instance config by referencing
self.render_config.encode_output_srgb (or note that it can be set at
construction via SensorTiledCamera.RenderConfig passed as config) and clarify
that update() itself does not accept a render_config argument—update the
parameter descriptions for color_image/depth_image accordingly.
In `@newton/_src/utils/import_usd.py`:
- Around line 527-528: The code currently calls _get_material_props_cached(prim)
for every uncached child (path_name) even for non-geometry prims; move the
shape_color lookup so it only runs after you detect a supported geometry type
for prim (e.g., inside the branches that handle Mesh/Sphere/Cube or after
checking prim.GetTypeName()/UsdGeom predicates), leaving the existing path_name
not in path_shape_map check in place but deferring shape_color resolution until
inside the geometry-specific branches where a shape is actually created; use the
same variables (path_name, prim, shape_color, _get_material_props_cached,
path_shape_map) to locate and update the logic.
In `@newton/tests/test_import_usd.py`:
- Line 20: The test imports the production function srgb_to_linear_rgb which
makes the test pass even if both test and production code regress; remove the
import of newton._src.utils.color.srgb_to_linear_rgb and replace usage with
either hard-coded reference output values (fixed decimals) or a small test-local
implementation of the sRGB→linear conversion inside
newton/tests/test_import_usd.py, and update the assertions to compare against
those fixed values; apply the same change to the other related test blocks
referenced (around the ranges 6537-6556, 6558-6585, 6587-6617, 6620-6651) so all
sRGB expectations are independent of production code.
In `@newton/tests/test_sensor_tiled_camera.py`:
- Line 12: The test imports and uses Newton’s linear_to_srgb_rgb() converter to
build the expected byte results, making the oracle dependent on production code;
remove the import of linear_to_srgb_rgb from test_sensor_tiled_camera.py and
replace the derived-expected logic (the code around where expected bytes are
computed) with either a small, self-contained local conversion implementation
(e.g., the sRGB gamma mapping applied to linear RGB values used in the test) or
with fixed hard-coded expected byte values; update the assertion to use that
local converter or fixed bytes and ensure you do not call linear_to_srgb_rgb()
from the production module in the test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: c144981f-eee0-4f5a-9c43-c0bbf810432d
📒 Files selected for processing (23)
CHANGELOG.mdnewton/_src/geometry/types.pynewton/_src/sensors/sensor_tiled_camera.pynewton/_src/sensors/warp_raytrace/render.pynewton/_src/sensors/warp_raytrace/render_context.pynewton/_src/sensors/warp_raytrace/textures.pynewton/_src/sensors/warp_raytrace/tiling.pynewton/_src/sensors/warp_raytrace/types.pynewton/_src/sensors/warp_raytrace/utils.pynewton/_src/sim/builder.pynewton/_src/sim/model.pynewton/_src/usd/utils.pynewton/_src/utils/color.pynewton/_src/utils/import_usd.pynewton/_src/utils/mesh.pynewton/_src/viewer/gl/opengl.pynewton/_src/viewer/gl/shaders.pynewton/_src/viewer/viewer.pynewton/_src/viewer/viewer_rerun.pynewton/_src/viewer/viewer_viser.pynewton/tests/test_import_usd.pynewton/tests/test_sensor_tiled_camera.pynewton/tests/test_shape_colors.py
| def _normalize_shape_color(color: Vec3 | None) -> tuple[float, float, float] | None: | ||
| if color is None: | ||
| return None | ||
| return (float(color[0]), float(color[1]), float(color[2])) |
There was a problem hiding this comment.
Validate malformed color inputs here.
default_shape_color is new public state, but this helper blindly indexes three components. A bad value like (1.0, 0.0) currently fails later with an IndexError instead of a clear ValueError.
Suggested fix
`@staticmethod`
def _normalize_shape_color(color: Vec3 | None) -> tuple[float, float, float] | None:
if color is None:
return None
- return (float(color[0]), float(color[1]), float(color[2]))
+ try:
+ return (float(color[0]), float(color[1]), float(color[2]))
+ except (IndexError, TypeError, ValueError) as e:
+ raise ValueError("Shape colors must provide at least three numeric components.") from e🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@newton/_src/sim/builder.py` around lines 197 - 200, The
_normalize_shape_color helper must validate inputs instead of blindly indexing:
in _normalize_shape_color (and callers like any use of default_shape_color),
check that color is a sequence/iterable of exactly three numeric components,
raise a clear ValueError for malformed inputs (e.g., wrong length or non-numeric
items), and then return the tuple of floats via float(component) conversions;
this ensures malformed values like (1.0, 0.0) produce a descriptive ValueError
rather than an IndexError.
| texture, texture_color_space = _find_texture_in_shader(source_shader, prim) | ||
| properties["texture"] = texture | ||
| properties["texture_color_space"] = texture_color_space |
There was a problem hiding this comment.
Propagate the resolved texture color space onto the returned Mesh.
These branches now resolve texture_color_space, but get_mesh() still never copies that value into mesh_out._texture_color_space. Direct newton.usd.get_mesh() / Mesh.create_from_usd() callers will therefore fall back to auto even when the USD material explicitly resolved raw.
💡 Suggested fix
mesh_out = Mesh(
points,
faces.flatten(),
normals=normals,
uvs=uvs,
maxhullvert=maxhullvert,
color=material_props.get("color"),
texture=material_props.get("texture"),
metallic=material_props.get("metallic"),
roughness=material_props.get("roughness"),
)
+ if mesh_out.texture is not None:
+ mesh_out._texture_color_space = material_props.get("texture_color_space", TEXTURE_COLOR_SPACE_AUTO)
if return_uv_indices:
return mesh_out, uv_indicesAlso applies to: 1843-1847
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@newton/_src/usd/utils.py` around lines 1687 - 1689, The resolved texture
color space stored in properties["texture_color_space"] is not being propagated
to the returned Mesh; modify get_mesh() (and the equivalent branch around
1843-1847) so after Mesh.create_from_usd(...) (or when populating mesh_out) you
set mesh_out._texture_color_space = properties.get("texture_color_space") (or
the resolved variable) before returning the mesh; ensure this assignment occurs
in both branches that call _find_texture_in_shader/source_shader so callers of
newton.usd.get_mesh() and Mesh.create_from_usd() receive the correct color space
instead of falling back to "auto".
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
## Motivation Seven `add_shape_*` methods (`add_shape_ellipsoid`, `add_shape_box`, `add_shape_capsule`, `add_shape_cylinder`, `add_shape_cone`, `add_shape_heightfield`, `add_shape_gaussian`) used the abbreviated phrase "uses the default per-shape color" for their `color` parameter docstring. This is ambiguous — it could mean the palette sequence or the new `default_shape_color` attribute. Meanwhile `add_shape_plane` and `add_shape_sphere` already used the explicit wording: ``` uses :attr:`default_shape_color` when set, otherwise the per-shape palette color. ``` ## What changed - Replaced the abbreviated `"uses the default per-shape color"` in all seven methods with the explicit two-line form that matches `add_shape_plane` and `add_shape_sphere`. - Mesh-backed shapes (`add_shape_mesh`, `add_shape_convex_hull`) are left unchanged — they correctly document the `:attr:`~newton.Mesh.color`` fallback instead. ## Why this fix is correct This is a doc-only change. All nine non-mesh `add_shape_*` methods delegate to `add_shape()` which implements the `color → default_shape_color → palette` chain. The docstrings now consistently describe that chain. ## Verification - `uv run --extra dev -m newton.tests -k test_shape_colors` — all 9 tests pass - Confirmed zero remaining occurrences of `"uses the default per-shape color"` via grep
…ce property ## Motivation Three groups of review findings addressed: 1. **Copyright year** (#53173, #53185): `newton/_src/utils/color.py` is a brand-new file introduced in this PR (reviewed 2026-04-10) but had `Copyright (c) 2025`. Per AGENTS.md, SPDX lines use the year the file was first created. 2. **Missing public re-export** (#53175, #53183, #53271): The CHANGELOG tells users to "convert any display/sRGB values to linear first" when writing to `Model.shape_color`, but `srgb_to_linear_rgb` and `linear_to_srgb_rgb` only existed in `newton._src.utils.color` — an internal module users must not import per project conventions. Users had no public tool to perform the conversion. 3. **Leaky `_texture_color_space` abstraction** (#53182): The private `_texture_color_space` attribute was accessed from 4 external call sites via `getattr(shape, "_texture_color_space", "auto")`, duplicating the default in every caller with no type safety. ## Changes - **`newton/_src/utils/color.py`**: Update SPDX year to 2026. - **`newton/utils.py`**: Re-export `srgb_to_linear_rgb` and `linear_to_srgb_rgb` in the public `__all__`. - **`CHANGELOG.md`**: Append `with newton.utils.srgb_to_linear_rgb()` to migration note. - **`newton/_src/geometry/types.py`**: Add `texture_color_space` as a `@property` with getter/setter on `Mesh`. Update `copy()` to use the property. - **`newton/_src/sensors/warp_raytrace/render_context.py`**: Replace two `getattr(..., "_texture_color_space", "auto")` calls with `shape.texture_color_space`. - **`newton/_src/viewer/viewer.py`**: Replace `getattr(geo_src, "_texture_color_space", "auto")` with `geo_src.texture_color_space`. - **`newton/_src/utils/import_usd.py`**: Replace `mesh._texture_color_space = ...` with `mesh.texture_color_space = ...`. - **`newton/tests/test_import_usd.py`**: Replace `mesh._texture_color_space` with `mesh.texture_color_space` in two test assertions. ## Verification ```bash uv run --extra dev -m newton.tests -k test_shape_colors # 9 tests OK uv run --extra dev python -m unittest \ newton.tests.test_import_usd.TestImportSampleAssetsComposition.test_visible_collision_mesh_inherits_visual_material_properties \ newton.tests.test_import_usd.TestImportSampleAssetsComposition.test_visible_collision_mesh_preserves_texture_color_space_metadata \ newton.tests.test_import_usd.TestImportSampleAssetsComposition.test_preview_surface_srgb_base_color_converts_to_linear \ newton.tests.test_import_usd.TestImportSampleAssetsComposition.test_display_color_srgb_metadata_converts_to_linear \ newton.tests.test_import_usd.TestImportSampleAssetsComposition.test_omnipbr_diffuse_tint_multiplies_authored_base_color # 5 tests OK uv run python -c "from newton.utils import srgb_to_linear_rgb, linear_to_srgb_rgb; ..." # public import OK uv run python -c "from newton import Mesh; m = Mesh(...); assert m.texture_color_space == 'auto'; ..." # property OK uv run python docs/generate_api.py # newton_utils.rst now includes 20 symbols ```
…lor_space ## Motivation `_texture_color_space` was introduced in this PR as a private attribute on `Mesh`, but every consumer outside `types.py` accessed it through `getattr(shape, "_texture_color_space", "auto")` — a defensive pattern that signals the attribute should just be public. There is no validation, transformation, or lazy-computation reason to hide it behind a `@property`. ## What changed - **`newton/_src/geometry/types.py`**: Renamed `self._texture_color_space` → `self.texture_color_space` in `__init__`, `copy()`, and the `texture` setter. Added an inline docstring on the attribute in `__init__`. - **`newton/_src/sensors/warp_raytrace/render_context.py`**: Replaced two `getattr(shape, "_texture_color_space", "auto")` calls with direct `shape.texture_color_space` access. - **`newton/_src/utils/import_usd.py`**: Changed `mesh._texture_color_space =` to `mesh.texture_color_space =`. - **`newton/_src/viewer/viewer.py`**: Changed `getattr(geo_src, "_texture_color_space", "auto")` to `geo_src.texture_color_space`. - **`newton/tests/test_import_usd.py`**: Updated two assertions from `mesh._texture_color_space` to `mesh.texture_color_space`. ## Why this fix is correct The attribute is a simple string (`"auto"`, `"srgb"`, or `"raw"`) with no invariants beyond what the texture setter resets. A plain public attribute is the simplest correct API. The `texture` setter still resets it to `"auto"` when a new texture is assigned, preserving the existing semantics. ## Verification - `uv run --extra dev -m newton.tests -k test_shape_colors` — 9 tests pass - `uv run --extra dev -m newton.tests -k "test_import_usd.TestImportSampleAssetsComposition"` — 36 tests pass (includes `test_visible_collision_mesh_preserves_texture_color_space_metadata`) - `uv run --extra dev -m newton.tests -k test_visible_collision` — 3 tests pass - Confirmed zero remaining `_texture_color_space` attribute references via grep
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
newton/_src/geometry/types.py (1)
877-879: Normalizetexture_color_spacein the setter to prevent texture-cache fragmentation.Storing raw values allows equivalent metadata tokens to diverge (
None/"auto"/vendor aliases), which can duplicate texture entries downstream.♻️ Proposed fix
-from ..utils.texture import compute_texture_hash +from ..utils.color import normalize_texture_color_space +from ..utils.texture import compute_texture_hash @@ `@texture_color_space.setter` def texture_color_space(self, value: str): - self._texture_color_space = value + self._texture_color_space = normalize_texture_color_space(value)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/geometry/types.py` around lines 877 - 879, The texture_color_space setter currently stores raw inputs which fragments the texture cache; update the texture_color_space setter to normalize inputs before assigning to self._texture_color_space: coerce None to a canonical token (e.g. "auto"), trim/normalize case, and map known vendor aliases or synonyms (e.g. "srgb", "sRGB", vendor-specific names) to a small set of canonical tokens; implement this normalization inside the texture_color_space property setter so callers use the normalized value consistently.newton/tests/test_import_usd.py (1)
6695-6723: Make the"auto"expectation explicit in the mock.This assertion is currently passing even if
resolve_material_properties_for_prim()regresses, because the mock never returns atexture_color_space. If this test is meant to pin the resolved default, include"texture_color_space": "auto"in the mocked payload instead of relying on downstream fallback.Suggested change
mock.patch( "newton._src.utils.import_usd.usd.resolve_material_properties_for_prim", return_value={ "color": None, "roughness": 0.35, "metallic": 0.75, "texture": "dummy.png", + "texture_color_space": "auto", }, ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/tests/test_import_usd.py` around lines 6695 - 6723, The mock for resolve_material_properties_for_prim used in the test should explicitly include the texture_color_space key to assert the resolved default; update the return_value in the mock patch for "newton._src.utils.import_usd.usd.resolve_material_properties_for_prim" to include "texture_color_space": "auto" so that the assertion comparing builder.shape_source[collision_shape].texture_color_space to "auto" is driven by the mocked payload (affecting the builder.add_usd flow and the shape source created by newton.ModelBuilder).newton/_src/utils/color.py (1)
26-80: Consider upgrading function docstrings to full Google style for user-facing helpers.These docstrings are clear, but adding
Args:/Returns:blocks would align with repository standards for parameterized APIs.As per coding guidelines, "Follow Google-style docstrings. Types in annotations, not docstrings. Use
Args:withname: descriptionformat."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/utils/color.py` around lines 26 - 80, Several public helper functions (srgb_to_linear_rgb, linear_to_srgb_rgb, linear_rgb_to_srgb_uint8, linear_image_to_srgb_uint8, normalize_texture_color_space) use short one-line docstrings; update each to Google-style docstrings that include an "Args:" section (parameter name: brief description, do NOT duplicate type info since types are in annotations) and a "Returns:" section describing the returned value/shape/units, and any raised exceptions (e.g., ValueError from linear_image_to_srgb_uint8). Keep descriptions concise and user-facing, follow existing wording but expand into the Args/Returns format to match repo guidelines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@newton/_src/utils/color.py`:
- Around line 69-75: The code currently classifies "identity" as auto; remove
"identity" from the first conditional tuple (token in ("",
TEXTURE_COLOR_SPACE_AUTO, "unknown", "identity")) and add it to the raw group
tuple used in the second conditional (token in (TEXTURE_COLOR_SPACE_RAW, "data",
"lin_rec709_scene") or token.startswith("lin_")), so that "identity" is treated
as TEXTURE_COLOR_SPACE_RAW; keep the existing startswith checks for lin_ and
srgb_ unchanged.
In `@newton/tests/test_import_usd.py`:
- Line 20: Replace the private-module import with the public re-export: change
the import that currently pulls srgb_to_linear_rgb from newton._src.utils.color
to use the public API (import srgb_to_linear_rgb from newton.utils), so the test
imports the exported helper symbol srgb_to_linear_rgb and will fail if the
public re-export breaks.
---
Nitpick comments:
In `@newton/_src/geometry/types.py`:
- Around line 877-879: The texture_color_space setter currently stores raw
inputs which fragments the texture cache; update the texture_color_space setter
to normalize inputs before assigning to self._texture_color_space: coerce None
to a canonical token (e.g. "auto"), trim/normalize case, and map known vendor
aliases or synonyms (e.g. "srgb", "sRGB", vendor-specific names) to a small set
of canonical tokens; implement this normalization inside the texture_color_space
property setter so callers use the normalized value consistently.
In `@newton/_src/utils/color.py`:
- Around line 26-80: Several public helper functions (srgb_to_linear_rgb,
linear_to_srgb_rgb, linear_rgb_to_srgb_uint8, linear_image_to_srgb_uint8,
normalize_texture_color_space) use short one-line docstrings; update each to
Google-style docstrings that include an "Args:" section (parameter name: brief
description, do NOT duplicate type info since types are in annotations) and a
"Returns:" section describing the returned value/shape/units, and any raised
exceptions (e.g., ValueError from linear_image_to_srgb_uint8). Keep descriptions
concise and user-facing, follow existing wording but expand into the
Args/Returns format to match repo guidelines.
In `@newton/tests/test_import_usd.py`:
- Around line 6695-6723: The mock for resolve_material_properties_for_prim used
in the test should explicitly include the texture_color_space key to assert the
resolved default; update the return_value in the mock patch for
"newton._src.utils.import_usd.usd.resolve_material_properties_for_prim" to
include "texture_color_space": "auto" so that the assertion comparing
builder.shape_source[collision_shape].texture_color_space to "auto" is driven by
the mocked payload (affecting the builder.add_usd flow and the shape source
created by newton.ModelBuilder).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: e7c058b6-1793-4655-9c58-9738de1498b1
📒 Files selected for processing (8)
CHANGELOG.mdnewton/_src/geometry/types.pynewton/_src/sensors/warp_raytrace/render_context.pynewton/_src/utils/color.pynewton/_src/utils/import_usd.pynewton/_src/viewer/viewer.pynewton/tests/test_import_usd.pynewton/utils.py
🚧 Files skipped from review as they are similar to previous changes (2)
- newton/_src/viewer/viewer.py
- CHANGELOG.md
| if token in ("", TEXTURE_COLOR_SPACE_AUTO, "unknown", "identity"): | ||
| return TEXTURE_COLOR_SPACE_AUTO | ||
| if token in (TEXTURE_COLOR_SPACE_RAW, "data", "lin_rec709_scene") or token.startswith("lin_"): | ||
| return TEXTURE_COLOR_SPACE_RAW | ||
| if token in (TEXTURE_COLOR_SPACE_SRGB, "srgb_rec709_scene", "g22_rec709_scene") or token.startswith("srgb_"): | ||
| return TEXTURE_COLOR_SPACE_SRGB | ||
| return TEXTURE_COLOR_SPACE_AUTO |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In OpenUSD / UsdUVTexture color-space metadata conventions, what does the token "identity" mean? Should it be treated as raw/no-op (linear) rather than sRGB/default display?
💡 Result:
In OpenUSD color-space metadata conventions, the token "identity" means the color space is unknown. It is a backwards-compatibility designation equivalent to the modern "unknown" token. Like "raw", it indicates no color transformation should be applied to the data (treated as raw/no-op, linear). It should not be treated as sRGB or any display color space with gamma correction; instead, pass the values through unchanged, as linear data.
Citations:
- 1: https://openusd.org/dev/user_guides/color_user_guide.html
- 2: https://www.openusd.org/dev/user_guides/color_user_guide.html
- 3: https://graphics.pixar.com/usd/release/user_guides/color_user_guide.html
- 4: https://openusd.org/release/user_guides/color_user_guide.html
🏁 Script executed:
# Read the full function around lines 69-85 to see the complete logic
wc -l newton/_src/utils/color.pyRepository: newton-physics/newton
Length of output: 1893
🏁 Script executed:
# Get context around lines 69-85 to see the complete color space mapping logic
sed -n '50,100p' newton/_src/utils/color.pyRepository: newton-physics/newton
Length of output: 1893
Move "identity" to the raw color-space group per OpenUSD conventions.
Per OpenUSD color-space specifications, "identity" is a backwards-compatibility token meaning "no color transformation"—equivalent to "unknown" but correctly classified as linear (raw), not auto. The current code incorrectly groups it with auto-mapped tokens, which causes downstream sRGB conversion.
Required fix
- if token in ("", TEXTURE_COLOR_SPACE_AUTO, "unknown", "identity"):
+ if token in ("", TEXTURE_COLOR_SPACE_AUTO, "unknown"):
return TEXTURE_COLOR_SPACE_AUTO
- if token in (TEXTURE_COLOR_SPACE_RAW, "data", "lin_rec709_scene") or token.startswith("lin_"):
+ if token in ("identity", TEXTURE_COLOR_SPACE_RAW, "data", "lin_rec709_scene") or token.startswith("lin_"):
return TEXTURE_COLOR_SPACE_RAW📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if token in ("", TEXTURE_COLOR_SPACE_AUTO, "unknown", "identity"): | |
| return TEXTURE_COLOR_SPACE_AUTO | |
| if token in (TEXTURE_COLOR_SPACE_RAW, "data", "lin_rec709_scene") or token.startswith("lin_"): | |
| return TEXTURE_COLOR_SPACE_RAW | |
| if token in (TEXTURE_COLOR_SPACE_SRGB, "srgb_rec709_scene", "g22_rec709_scene") or token.startswith("srgb_"): | |
| return TEXTURE_COLOR_SPACE_SRGB | |
| return TEXTURE_COLOR_SPACE_AUTO | |
| if token in ("", TEXTURE_COLOR_SPACE_AUTO, "unknown"): | |
| return TEXTURE_COLOR_SPACE_AUTO | |
| if token in ("identity", TEXTURE_COLOR_SPACE_RAW, "data", "lin_rec709_scene") or token.startswith("lin_"): | |
| return TEXTURE_COLOR_SPACE_RAW | |
| if token in (TEXTURE_COLOR_SPACE_SRGB, "srgb_rec709_scene", "g22_rec709_scene") or token.startswith("srgb_"): | |
| return TEXTURE_COLOR_SPACE_SRGB | |
| return TEXTURE_COLOR_SPACE_AUTO |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@newton/_src/utils/color.py` around lines 69 - 75, The code currently
classifies "identity" as auto; remove "identity" from the first conditional
tuple (token in ("", TEXTURE_COLOR_SPACE_AUTO, "unknown", "identity")) and add
it to the raw group tuple used in the second conditional (token in
(TEXTURE_COLOR_SPACE_RAW, "data", "lin_rec709_scene") or
token.startswith("lin_")), so that "identity" is treated as
TEXTURE_COLOR_SPACE_RAW; keep the existing startswith checks for lin_ and srgb_
unchanged.
| from newton import BodyFlags, JointType | ||
| from newton._src.geometry.flags import ShapeFlags | ||
| from newton._src.geometry.utils import transform_points | ||
| from newton._src.utils.color import srgb_to_linear_rgb |
There was a problem hiding this comment.
Import the public conversion helper here.
Line 20 reaches into newton._src, so these tests bypass the new newton.utils.srgb_to_linear_rgb re-export this PR is adding. A broken public export would still pass this file.
Suggested change
-from newton._src.utils.color import srgb_to_linear_rgb
+from newton.utils import srgb_to_linear_rgb🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@newton/tests/test_import_usd.py` at line 20, Replace the private-module
import with the public re-export: change the import that currently pulls
srgb_to_linear_rgb from newton._src.utils.color to use the public API (import
srgb_to_linear_rgb from newton.utils), so the test imports the exported helper
symbol srgb_to_linear_rgb and will fail if the public re-export breaks.
Keep builder, importer, and viewer shape and mesh colors in authored display space instead of pre-linearizing them on ingest. Convert to linear only at shading boundaries and update docs and tests to match.
…ce-conventions # Conflicts: # newton/_src/sim/builder.py # newton/_src/usd/utils.py # newton/_src/utils/import_usd.py # newton/tests/test_sensor_tiled_camera.py
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
newton/_src/utils/mesh.py (1)
428-474:⚠️ Potential issue | 🟠 MajorGating authored-linear conversion solely by
.daeextension misses glTF/GLB formats.
_extract_trimesh_material_paramsis the shared path for all formats trimesh can load, butauthored_linearis driven byauthored_linear_material_colors = filename.lower().endswith(".dae")(line 629). Per the glTF 2.0 specification,baseColorFactoris defined in linear space. When a.gltf/.glbis loaded through trimesh, this function will read the linearbaseColorFactor, but withauthored_linear=False, nolinear_to_srgb_rgbconversion is applied. The storedMesh.coloris then treated as sRGB by the GL shader (srgb_to_linear(ObjectColor)atshaders.py:283), which would darken/shift glTF material colors incorrectly.Since this PR explicitly addresses color-space conventions (per the branch name), consider broadening the gate to detect
.gltf/.glbextensions or drivingauthored_linearfrom per-material metadata (baseColorFactor vs. baseColorTexture). Alternatively, confirm if glTF support is intentionally scoped out of this change.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/utils/mesh.py` around lines 428 - 474, The function _extract_trimesh_material_params currently only applies linear->sRGB conversion when authored_linear is set (driven by authored_linear_material_colors = filename.lower().endswith(".dae")), which misses glTF (.gltf/.glb) where baseColorFactor is authored in linear space; change the gate where authored_linear is computed to also return True for filenames ending with ".gltf" or ".glb" (or alternatively derive authored_linear per-material by checking for presence of baseColorFactor on the material before calling _extract_trimesh_material_params), and ensure the code path that calls linear_to_srgb_rgb(base_color) remains unchanged inside _extract_trimesh_material_params so glTF baseColorFactor values are converted to sRGB when appropriate.
♻️ Duplicate comments (2)
newton/_src/sim/builder.py (1)
196-199:⚠️ Potential issue | 🟡 MinorReturn a clear error for malformed
default_shape_color.
default_shape_coloris public state, but malformed values still flow into_coerce_shape_color()and can raiseIndexError/low-level conversion errors while adding a shape. Please wrap coercion failures in a descriptiveValueError.Suggested fix
def _coerce_shape_color(color: Vec3 | None) -> tuple[float, float, float] | None: if color is None: return None - return (float(color[0]), float(color[1]), float(color[2])) + try: + return (float(color[0]), float(color[1]), float(color[2])) + except (IndexError, TypeError, ValueError) as e: + raise ValueError("Shape colors must provide three numeric RGB components.") from eAlso applies to: 797-800, 5298-5299
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/sim/builder.py` around lines 196 - 199, The _coerce_shape_color function should catch malformed inputs and raise a clear ValueError instead of letting IndexError/TypeError/ValueError propagate; modify _coerce_shape_color to validate and/or wrap the tuple conversion in a try/except that catches IndexError, TypeError and ValueError and re-raises a descriptive ValueError that includes the invalid default_shape_color value and the original exception message. Apply the same defensive wrapping to any other spots where the same coercion logic is implemented or called (the other occurrences noted in the review) so callers receive a consistent, user-friendly error.newton/_src/usd/utils.py (1)
1120-1135:⚠️ Potential issue | 🟠 Major
mesh_out._texture_color_spaceis still not populated frommaterial_props.
resolve_material_properties_for_prim()now returns"texture_color_space", butMesh(...)here doesn't receive it (no constructor arg) and the attribute isn't assigned post-construction. Downstream consumers — notably the viewer'stexture_modebranch innewton/_src/viewer/viewer.pyL1580-1582 — rely onMesh.texture_color_space == "raw"to avoid the sRGB→linear shader path, so the raw branch will never fire for USD-imported meshes until this is wired through.💡 Suggested fix
mesh_out = Mesh( points, faces.flatten(), normals=normals, uvs=uvs, maxhullvert=maxhullvert, color=material_props.get("color"), texture=material_props.get("texture"), metallic=material_props.get("metallic"), roughness=material_props.get("roughness"), ) + if mesh_out.texture is not None: + mesh_out._texture_color_space = material_props.get( + "texture_color_space", TEXTURE_COLOR_SPACE_AUTO + )If
Mesh.__init__acceptstexture_color_spaceas a keyword, prefer passing it there instead of touching the private attribute.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/usd/utils.py` around lines 1120 - 1135, The Mesh created from resolve_material_properties_for_prim(prim) is not getting the returned "texture_color_space" so mesh_out.texture_color_space remains unset; update the Mesh construction in utils.py to accept and set texture_color_space from material_props (prefer passing texture_color_space=material_props.get("texture_color_space") into the Mesh(...) call if Mesh.__init__ supports it, otherwise assign mesh_out.texture_color_space = material_props.get("texture_color_space") immediately after construction) so downstream logic (e.g. the viewer's texture_mode branch that checks Mesh.texture_color_space == "raw") behaves correctly.
🧹 Nitpick comments (6)
newton/_src/viewer/viewer_viser.py (1)
575-579: LGTM — conversion aligns with the new color-space convention.Using
srgb_image_to_uint8here correctly clips to [0, 1], rounds, and casts to uint8, replacing ad-hoc*255scaling. Input shape(N, 3)satisfies the helper's contract.Minor observation (out of scope for this change):
log_points(Lines 960-970) and the_rgb_to_uint8_arrayhelper inlog_lines(Lines 832-837) still perform manual float→uint8 scaling and don't go through the shared helper. Worth unifying in a follow-up for consistency with the PR's color-space rules.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/viewer/viewer_viser.py` around lines 575 - 579, Update the manual float→uint8 conversions to use the shared helper srgb_image_to_uint8: replace the ad-hoc scaling in the log_points function and the _rgb_to_uint8_array helper used by log_lines with calls to srgb_image_to_uint8 so clipping/rounding/casting follow the new color-space convention; ensure inputs are shaped/reshaped to (N,3) as required and preserve existing return types and caching behavior in log_lines and log_points.newton/_src/geometry/types.py (2)
183-186: Stray string literal after assignment — not a real docstring.The triple-quoted string on Line 186 follows a plain attribute assignment and is not attached to anything; Sphinx/help will never see it. The authoritative docstring already lives on the
texture_color_spaceproperty (Lines 932-937), so this adds nothing and can be misleading when grepping the class for attribute docs.🧹 Optional cleanup
self._texture = _normalize_texture_input(texture) - self.texture_color_space: str = "auto" - """Source color space of :attr:`texture`: ``"auto"``, ``"srgb"``, or ``"raw"``.""" + # Initializes self._texture_color_space via the property setter below. + self.texture_color_space = "auto"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/geometry/types.py` around lines 183 - 186, Remove the stray triple-quoted string literal that sits immediately after the attribute assignment to self.texture_color_space in the class initializer; it is not a real docstring and should be deleted so it doesn't clutter searches or confuse readers—locate the block where self._texture is set and texture_color_space: str = "auto" is assigned and remove the following standalone triple-quoted string (the genuine docstring lives on the texture_color_space property further down).
930-942: Consider validatingtexture_color_spacevalues.The property docstring narrows the allowed values to
{"auto", "srgb", "raw"}, and downstream consumers branch on these literals (raytracer dedup key / sRGB-vs-raw sampling). The setter currently accepts any string, so typos like"sRGB"or"linear"will silently fall through as if they were"auto"in some code paths and as unknown in others. A simple allow-list check in the setter would make misuse fail fast.🛡️ Suggested validation
`@texture_color_space.setter` def texture_color_space(self, value: str): + allowed = ("auto", "srgb", "raw") + if value not in allowed: + raise ValueError( + f"texture_color_space must be one of {allowed}, got {value!r}" + ) self._texture_color_space = value🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/geometry/types.py` around lines 930 - 942, The texture_color_space setter currently accepts any string; add validation in the texture_color_space setter to only allow the literals "auto", "srgb", and "raw" (optionally normalizing input to lowercase first), and raise a ValueError with a clear message if an invalid value is passed; update the internal field self._texture_color_space only after validation so consumers that branch on texture_color_space see only the allowed values (refer to the texture_color_space property/setter and the _texture_color_space attribute).newton/_src/viewer/viewer.py (1)
1588-1588: Reaching intoModelBuilder._DEFAULT_GROUND_PLANE_COLORcouples the viewer to a private constant.Consider exposing the default ground-plane color through a public surface (e.g., a
ModelBuilder.DEFAULT_GROUND_PLANE_COLORclassvar, a module-level constant innewton/_src/sim/builder.py, or a helper on the builder) so the viewer doesn't depend on a leading-underscore symbol. The equivalent default is already duplicated at line 820 aswp.vec3(0.125, 0.125, 0.25)(note the different third component) — unifying these two constants avoids them drifting apart further.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/viewer/viewer.py` at line 1588, The viewer is reading the private constant ModelBuilder._DEFAULT_GROUND_PLANE_COLOR which couples viewer.py to a leading-underscore symbol and duplicates a similar constant elsewhere; change the builder to expose a public constant (e.g., add ModelBuilder.DEFAULT_GROUND_PLANE_COLOR or export a module-level DEFAULT_GROUND_PLANE_COLOR in newton/_src/sim/builder.py) and update viewer.py to use that public symbol instead of ModelBuilder._DEFAULT_GROUND_PLANE_COLOR, and also consolidate the duplicated value (the wp.vec3(0.125, 0.125, 0.25) definition) so both locations reference the single public constant.newton/_src/usd/utils.py (1)
1607-1617: Dead conditional and an unexplained0.18fudge factor in_multiply_colors.Two things:
- Line 1615
... if tint is not None else Noneis unreachable — the early return on line 1612 already guaranteestint is not Noneat this point.0.18is OmniPBR's implicit middle-grey base assumption when only a tint is authored. Without a comment this reads as a magic number; it also silently fabricates a color that will then block thedisplayColorfallback in_resolve_prim_material_properties(line 1948 checksproperties["color"] is None). Worth deciding whether tint-only materials should synthesize a mid-grey base or defer todisplayColor— currently they do the former and skip the latter.♻️ Suggested cleanup
-def _multiply_colors( - color: tuple[float, float, float] | None, - tint: tuple[float, float, float] | None, -) -> tuple[float, float, float] | None: - """Multiply two RGB colors componentwise.""" - if tint is None: - return color - if color is None: - return tuple(t * 0.18 for t in tint) if tint is not None else None - return tuple(float(c * t) for c, t in zip(color, tint, strict=True)) +# OmniPBR assumes an 18% middle-grey diffuse reflectance when no base color +# is authored but a diffuse tint is present. +_OMNI_PBR_DEFAULT_DIFFUSE = 0.18 + +def _multiply_colors( + color: tuple[float, float, float] | None, + tint: tuple[float, float, float] | None, +) -> tuple[float, float, float] | None: + """Multiply two RGB colors componentwise. + + If only ``tint`` is authored, treat the base as OmniPBR's 18% middle grey. + """ + if tint is None: + return color + if color is None: + return tuple(float(t * _OMNI_PBR_DEFAULT_DIFFUSE) for t in tint) + return tuple(float(c * t) for c, t in zip(color, tint, strict=True))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/usd/utils.py` around lines 1607 - 1617, The helper _multiply_colors has an unreachable conditional and a magic 0.18 factor; update the function to (a) remove the dead `if tint is not None else None` branch and simplify the early returns, and (b) choose one behavior for tint-only inputs: either synthesize a documented mid-grey by replacing the literal 0.18 with a named constant (e.g. MID_GREY_FACTOR) and add a comment explaining OmniPBR middle-grey semantics, or return None so that `_resolve_prim_material_properties` can fall back to `displayColor`; also mention `_resolve_prim_material_properties` in the comment so future readers know why the choice matters.newton/tests/test_sensor_tiled_camera.py (1)
12-12: Use the public color utility import.This test is validating public tiled-camera color behavior, so it should avoid binding to the private
_srclayout.Suggested change
-from newton._src.utils.color import srgb_to_linear_rgb +from newton.utils import srgb_to_linear_rgbAs per coding guidelines, “Do not import from
newton._srcin examples and docs. Expose user-facing symbols via public modules (newton/geometry.py,newton/solvers.py, etc.).”🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/tests/test_sensor_tiled_camera.py` at line 12, The test imports srgb_to_linear_rgb from the private path newton._src.utils.color; update the import to use the public API instead by importing srgb_to_linear_rgb from the top-level public module that re-exports it (e.g., from newton.color or another public module that exposes srgb_to_linear_rgb), so change the import of srgb_to_linear_rgb to use the public symbol and remove any direct references to newton._src in test_sensor_tiled_camera.py.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@newton/_src/usd/utils.py`:
- Around line 1940-1953: The current guard skips the displayColor fallback when
_extract_shader_properties has already applied a synthetic tint-only color;
change the flow so the displayColor is used whenever the shader provided no real
base color or texture. Update _extract_shader_properties to return an explicit
flag (e.g., base_color_is_tint_only or has_real_base_color) alongside properties
when it generated a tint-only synthetic color, and then in this block (after
calling _extract_shader_properties and _extract_material_input_properties) check
that flag (or absence of a real base color) in addition to properties["texture"]
being None before skipping displayColor; if the shader lacked a real base color
and no texture exists, read displayColor via
UsdGeom.PrimvarsAPI(target_prim).GetPrimvar("displayColor") and set
properties["color"] = _coerce_color(...). Ensure the new flag name appears in
the returned properties so callers can detect tint-only cases.
In `@newton/tests/test_sensor_tiled_camera.py`:
- Around line 192-219: The new test
test_albedo_output_linearizes_srgb_shape_colors_only_once constructs and updates
SensorTiledCamera and must be CUDA-gated like the other rendering tests in this
file; add the same skip guard used elsewhere (e.g., the pytest.skip/skipIf or
has_cuda() check used by the other tests) at the start of
test_albedo_output_linearizes_srgb_shape_colors_only_once so the test is skipped
when CUDA is unavailable, keeping the rest of the test body unchanged.
---
Outside diff comments:
In `@newton/_src/utils/mesh.py`:
- Around line 428-474: The function _extract_trimesh_material_params currently
only applies linear->sRGB conversion when authored_linear is set (driven by
authored_linear_material_colors = filename.lower().endswith(".dae")), which
misses glTF (.gltf/.glb) where baseColorFactor is authored in linear space;
change the gate where authored_linear is computed to also return True for
filenames ending with ".gltf" or ".glb" (or alternatively derive authored_linear
per-material by checking for presence of baseColorFactor on the material before
calling _extract_trimesh_material_params), and ensure the code path that calls
linear_to_srgb_rgb(base_color) remains unchanged inside
_extract_trimesh_material_params so glTF baseColorFactor values are converted to
sRGB when appropriate.
---
Duplicate comments:
In `@newton/_src/sim/builder.py`:
- Around line 196-199: The _coerce_shape_color function should catch malformed
inputs and raise a clear ValueError instead of letting
IndexError/TypeError/ValueError propagate; modify _coerce_shape_color to
validate and/or wrap the tuple conversion in a try/except that catches
IndexError, TypeError and ValueError and re-raises a descriptive ValueError that
includes the invalid default_shape_color value and the original exception
message. Apply the same defensive wrapping to any other spots where the same
coercion logic is implemented or called (the other occurrences noted in the
review) so callers receive a consistent, user-friendly error.
In `@newton/_src/usd/utils.py`:
- Around line 1120-1135: The Mesh created from
resolve_material_properties_for_prim(prim) is not getting the returned
"texture_color_space" so mesh_out.texture_color_space remains unset; update the
Mesh construction in utils.py to accept and set texture_color_space from
material_props (prefer passing
texture_color_space=material_props.get("texture_color_space") into the Mesh(...)
call if Mesh.__init__ supports it, otherwise assign mesh_out.texture_color_space
= material_props.get("texture_color_space") immediately after construction) so
downstream logic (e.g. the viewer's texture_mode branch that checks
Mesh.texture_color_space == "raw") behaves correctly.
---
Nitpick comments:
In `@newton/_src/geometry/types.py`:
- Around line 183-186: Remove the stray triple-quoted string literal that sits
immediately after the attribute assignment to self.texture_color_space in the
class initializer; it is not a real docstring and should be deleted so it
doesn't clutter searches or confuse readers—locate the block where self._texture
is set and texture_color_space: str = "auto" is assigned and remove the
following standalone triple-quoted string (the genuine docstring lives on the
texture_color_space property further down).
- Around line 930-942: The texture_color_space setter currently accepts any
string; add validation in the texture_color_space setter to only allow the
literals "auto", "srgb", and "raw" (optionally normalizing input to lowercase
first), and raise a ValueError with a clear message if an invalid value is
passed; update the internal field self._texture_color_space only after
validation so consumers that branch on texture_color_space see only the allowed
values (refer to the texture_color_space property/setter and the
_texture_color_space attribute).
In `@newton/_src/usd/utils.py`:
- Around line 1607-1617: The helper _multiply_colors has an unreachable
conditional and a magic 0.18 factor; update the function to (a) remove the dead
`if tint is not None else None` branch and simplify the early returns, and (b)
choose one behavior for tint-only inputs: either synthesize a documented
mid-grey by replacing the literal 0.18 with a named constant (e.g.
MID_GREY_FACTOR) and add a comment explaining OmniPBR middle-grey semantics, or
return None so that `_resolve_prim_material_properties` can fall back to
`displayColor`; also mention `_resolve_prim_material_properties` in the comment
so future readers know why the choice matters.
In `@newton/_src/viewer/viewer_viser.py`:
- Around line 575-579: Update the manual float→uint8 conversions to use the
shared helper srgb_image_to_uint8: replace the ad-hoc scaling in the log_points
function and the _rgb_to_uint8_array helper used by log_lines with calls to
srgb_image_to_uint8 so clipping/rounding/casting follow the new color-space
convention; ensure inputs are shaped/reshaped to (N,3) as required and preserve
existing return types and caching behavior in log_lines and log_points.
In `@newton/_src/viewer/viewer.py`:
- Line 1588: The viewer is reading the private constant
ModelBuilder._DEFAULT_GROUND_PLANE_COLOR which couples viewer.py to a
leading-underscore symbol and duplicates a similar constant elsewhere; change
the builder to expose a public constant (e.g., add
ModelBuilder.DEFAULT_GROUND_PLANE_COLOR or export a module-level
DEFAULT_GROUND_PLANE_COLOR in newton/_src/sim/builder.py) and update viewer.py
to use that public symbol instead of ModelBuilder._DEFAULT_GROUND_PLANE_COLOR,
and also consolidate the duplicated value (the wp.vec3(0.125, 0.125, 0.25)
definition) so both locations reference the single public constant.
In `@newton/tests/test_sensor_tiled_camera.py`:
- Line 12: The test imports srgb_to_linear_rgb from the private path
newton._src.utils.color; update the import to use the public API instead by
importing srgb_to_linear_rgb from the top-level public module that re-exports it
(e.g., from newton.color or another public module that exposes
srgb_to_linear_rgb), so change the import of srgb_to_linear_rgb to use the
public symbol and remove any direct references to newton._src in
test_sensor_tiled_camera.py.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: c295c2d0-23e2-4c04-b6f6-7b1c51912108
📒 Files selected for processing (20)
CHANGELOG.mddocs/concepts/conventions.rstnewton/_src/geometry/types.pynewton/_src/sensors/sensor_tiled_camera.pynewton/_src/sensors/warp_raytrace/render.pynewton/_src/sim/builder.pynewton/_src/sim/model.pynewton/_src/usd/utils.pynewton/_src/utils/color.pynewton/_src/utils/import_usd.pynewton/_src/utils/mesh.pynewton/_src/viewer/gl/opengl.pynewton/_src/viewer/gl/shaders.pynewton/_src/viewer/viewer.pynewton/_src/viewer/viewer_rerun.pynewton/_src/viewer/viewer_viser.pynewton/examples/basic/example_basic_viewer.pynewton/tests/test_import_usd.pynewton/tests/test_sensor_tiled_camera.pynewton/tests/test_shape_colors.py
✅ Files skipped from review due to trivial changes (7)
- newton/examples/basic/example_basic_viewer.py
- newton/_src/viewer/gl/opengl.py
- newton/_src/sim/model.py
- docs/concepts/conventions.rst
- newton/_src/sensors/sensor_tiled_camera.py
- newton/_src/utils/import_usd.py
- newton/_src/utils/color.py
🚧 Files skipped from review as they are similar to previous changes (3)
- CHANGELOG.md
- newton/_src/viewer/viewer_rerun.py
- newton/_src/sensors/warp_raytrace/render.py
|
|
||
| # Always call _extract_shader_properties even if shader_id is None because | ||
| # it has fallback logic for common shader input names. | ||
| properties = _extract_shader_properties(source_shader, target_prim) | ||
| # it has fallback logic for common shader input names and material tints. | ||
| properties = _extract_shader_properties(source_shader, target_prim, material) | ||
| material_props = _extract_material_input_properties(material, target_prim) | ||
| for key in ("texture", "color", "metallic", "roughness"): | ||
| for key in ("texture", "texture_color_space", "color", "metallic", "roughness"): | ||
| if properties.get(key) is None and material_props.get(key) is not None: | ||
| properties[key] = material_props[key] | ||
| if properties["color"] is None and properties["texture"] is None: | ||
| display_color = UsdGeom.PrimvarsAPI(target_prim).GetPrimvar("displayColor") | ||
| if display_color: | ||
| properties["color"] = _coerce_color(display_color.Get()) | ||
| properties["color"] = _coerce_color(display_color.Get(), display_color.GetAttr()) | ||
|
|
||
| return properties |
There was a problem hiding this comment.
Tint-only materials bypass the displayColor fallback.
_extract_shader_properties multiplies in the diffuse tint at line 1816, so for materials that author only a tint (no baseColor, no texture), properties["color"] becomes the synthetic tint * 0.18. That then fails the properties["color"] is None and properties["texture"] is None guard here, so the displayColor primvar fallback is skipped even when the prim authored a more meaningful display color.
If the intent is "prefer displayColor when the shader has no real base color," consider either (a) deferring the tint multiply until after display-color resolution, or (b) checking displayColor whenever the shader base color was missing regardless of tint.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@newton/_src/usd/utils.py` around lines 1940 - 1953, The current guard skips
the displayColor fallback when _extract_shader_properties has already applied a
synthetic tint-only color; change the flow so the displayColor is used whenever
the shader provided no real base color or texture. Update
_extract_shader_properties to return an explicit flag (e.g.,
base_color_is_tint_only or has_real_base_color) alongside properties when it
generated a tint-only synthetic color, and then in this block (after calling
_extract_shader_properties and _extract_material_input_properties) check that
flag (or absence of a real base color) in addition to properties["texture"]
being None before skipping displayColor; if the shader lacked a real base color
and no texture exists, read displayColor via
UsdGeom.PrimvarsAPI(target_prim).GetPrimvar("displayColor") and set
properties["color"] = _coerce_color(...). Ensure the new flag name appears in
the returned properties so callers can detect tint-only cases.
| def test_albedo_output_linearizes_srgb_shape_colors_only_once(self): | ||
| color = (0.25, 0.5, 0.75) | ||
| model = self._build_single_sphere_scene(color) | ||
|
|
||
| camera_transforms = wp.array( | ||
| [[wp.transformf(wp.vec3f(0.0, 0.0, 0.0), wp.quatf(0.0, 0.0, 0.0, 1.0))]], | ||
| dtype=wp.transformf, | ||
| device="cpu", | ||
| ) | ||
|
|
||
| for encode_output_srgb in (True, False): | ||
| sensor = SensorTiledCamera( | ||
| model=model, | ||
| config=SensorTiledCamera.RenderConfig(encode_output_srgb=encode_output_srgb), | ||
| ) | ||
| camera_rays = sensor.utils.compute_pinhole_camera_rays(1, 1, math.radians(30.0)) | ||
| albedo_image = sensor.utils.create_albedo_image_output(1, 1, camera_count=1) | ||
| sensor.update(model.state(), camera_transforms, camera_rays, albedo_image=albedo_image) | ||
|
|
||
| packed = self._unpack_rgba(albedo_image.numpy()[0, 0, 0, 0]) | ||
| expected_rgb = ( | ||
| np.clip(np.asarray(color) * 255.0, 0.0, 255.0).astype(np.uint8) | ||
| if encode_output_srgb | ||
| else np.clip(np.asarray(srgb_to_linear_rgb(color)) * 255.0, 0.0, 255.0).astype(np.uint8) | ||
| ) | ||
|
|
||
| np.testing.assert_array_equal(packed[:3], expected_rgb) | ||
| self.assertEqual(packed[3], 255) |
There was a problem hiding this comment.
Keep this SensorTiledCamera render test CUDA-gated.
Unlike the other rendering tests in this file, this new test runs on CPU-only environments but still constructs and updates SensorTiledCamera. Add the same skip guard to avoid CPU-only CI failures.
Suggested change
+ `@unittest.skipUnless`(wp.is_cuda_available(), "Requires CUDA")
def test_albedo_output_linearizes_srgb_shape_colors_only_once(self):🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@newton/tests/test_sensor_tiled_camera.py` around lines 192 - 219, The new
test test_albedo_output_linearizes_srgb_shape_colors_only_once constructs and
updates SensorTiledCamera and must be CUDA-gated like the other rendering tests
in this file; add the same skip guard used elsewhere (e.g., the
pytest.skip/skipIf or has_cuda() check used by the other tests) at the start of
test_albedo_output_linearizes_srgb_shape_colors_only_once so the test is skipped
when CUDA is unavailable, keeping the rest of the test body unchanged.
Remove the temporary ModelBuilder.default_shape_color fallback so this PR stays focused on Model.shape_color display-RGB storage. Also revert no-op import_usd keyword-order churn and keep the changelog/docs scoped to the shape_color behavior change.
Keep the shape-color PR focused by dropping wrap-only docstring edits and other cosmetic changes that obscured the actual behavior updates.
Description
ModelBuilder/Modelshape colors in display RGB instead of pre-linearizing them on ingestRender Comparison
Headless
example_robot_panda_render.pycapture using the same Panda USD asset and camera onorigin/mainand this branch.Checklist
CHANGELOG.mdhas been updated (if user-facing change)Test plan
Bug fix
Steps to reproduce:
builder.shape_coloror render the imported scene in ViewerGL /SensorTiledCamera.Minimal reproduction:
New feature / API change
Summary by CodeRabbit
Release Notes
New Features
"auto","srgb", and"raw"options.encode_output_srgbsetting.linear_to_srgb_rgb()andsrgb_to_linear_rgb().Documentation
Tests